﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;

namespace YoutubeTranscriptApi
{
    // https://github.com/jdepoix/youtube-transcript-api/blob/master/youtube_transcript_api/_api.py

    /// <summary>
    /// YouTube transcript api.
    /// </summary>
    public sealed class YouTubeTranscriptApi : IDisposable
    {
        private readonly HttpClient _httpClient;
        private readonly HttpClientHandler _httpHandler;

        /// <summary>
        /// Initializes a new instance of the <see cref="YouTubeTranscriptApi"/> class.
        /// </summary>
        public YouTubeTranscriptApi()
        {
            _httpHandler = new HttpClientHandler() { CookieContainer = new CookieContainer() };
            _httpClient = new HttpClient(_httpHandler, true);
        }

        /// <summary>
        ///      Retrieves the list of transcripts which are available for a given video. It returns a `TranscriptList` object
        ///        which is iterable and provides methods to filter the list of transcripts for specific languages. While iterating
        ///        over the `TranscriptList` the individual transcripts are represented by `Transcript` objects, which provide
        ///        metadata and can either be fetched by calling `transcript.fetch()` or translated by calling
        ///        `transcript.translate('en')`. Example::
        ///            # retrieve the available transcripts
        ///            transcript_list = YouTubeTranscriptApi.get('video_id')
        ///            # iterate over all available transcripts
        ///            for transcript in transcript_list:
        ///                # the Transcript object provides metadata properties
        ///                print(
        ///                    transcript.video_id,
        ///                    transcript.language,
        ///                    transcript.language_code,
        ///                    # whether it has been manually created or generated by YouTube
        ///                    transcript.is_generated,
        ///                    # a list of languages the transcript can be translated to
        ///                    transcript.translation_languages,
        ///                )
        ///                # fetch the actual transcript data
        ///                print(transcript.fetch())
        ///                # translating the transcript will return another transcript object
        ///                print(transcript.translate('en').fetch())
        ///            # you can also directly filter for the language you are looking for, using the transcript list
        ///            transcript = transcript_list.find_transcript(['de', 'en'])
        ///            # or just filter for manually created transcripts
        ///            transcript = transcript_list.find_manually_created_transcript(['de', 'en'])
        ///            # or automatically generated ones
        ///            transcript = transcript_list.find_generated_transcript(['de', 'en'])
        /// </summary>
        /// <param name="video_id">the youtube video id</param>
        /// <param name="proxies">a dictionary mapping of http and https proxies to be used for the network requests</param>
        /// <param name="cookies">a string of the path to a text file containing youtube authorization cookies</param>
        /// <returns>the list of available transcripts</returns>
        public TranscriptList ListTranscripts(string video_id, Dictionary<string, string> proxies = null, string cookies = null)
        {
            if (cookies != null)
            {
                _httpHandler.CookieContainer.Add(loadCookies(cookies, video_id));
            }

            //    http_client.proxies = proxies if proxies else {}
            return new TranscriptListFetcher(_httpClient, _httpHandler).Fetch(video_id);
        }

        /// <summary>
        /// Retrieves the transcripts for a list of videos.
        /// </summary>
        /// <param name="video_ids">a list of youtube video ids</param>
        /// <param name="languages">A list of language codes in a descending priority. For example, if this is set to ['de', 'en']
        /// it will first try to fetch the german transcript(de) and then fetch the english transcript(en) if it fails to
        /// do so.</param>
        /// <param name="continue_after_error">if this is set the execution won't be stopped, if an error occurs while retrieving
        /// one of the video transcripts</param>
        /// <param name="proxies">a dictionary mapping of http and https proxies to be used for the network requests</param>
        /// <param name="cookies">a string of the path to a text file containing youtube authorization cookies</param>
        /// <returns>a tuple containing a dictionary mapping video ids onto their corresponding transcripts, and a list of
        /// video ids, which could not be retrieved</returns>
        public (Dictionary<string, IEnumerable<TranscriptItem>>, IReadOnlyList<string>) GetTranscripts(IReadOnlyList<string> video_ids, IReadOnlyList<string> languages = null, bool continue_after_error = false, Dictionary<string, string> proxies = null, string cookies = null)
        {
            //     :param proxies: a dictionary mapping of http and https proxies to be used for the network requests
            //     :type proxies: { 'http': str, 'https': str} -http://docs.python-requests.org/en/master/user/advanced/#proxies
            if (languages == null)
            {
                languages = new List<string>() { "en" };
            }

            var data = new Dictionary<string, IEnumerable<TranscriptItem>>();
            var unretrievable_videos = new List<string>();

            foreach (var video_id in video_ids)
            {
                try
                {
                    data[video_id] = GetTranscript(video_id, languages, proxies, cookies);
                }
                catch (Exception)
                {
                    if (!continue_after_error) throw;
                    unretrievable_videos.Add(video_id);
                }
            }

            return (data, unretrievable_videos);
        }

        /// <summary>
        /// Retrieves the transcript for a single video. This is just a shortcut for calling::
        ///<para>YouTubeTranscriptApi.list_transcripts(video_id, proxies).find_transcript(languages).fetch()</para>
        /// </summary>
        /// <param name="video_id">the youtube video id</param>
        /// <param name="languages">A list of language codes in a descending priority. For example, if this is set to ['de', 'en']
        /// it will first try to fetch the german transcript(de) and then fetch the english transcript(en) if it fails to
        /// do so.</param>
        /// <param name="proxies">a dictionary mapping of http and https proxies to be used for the network requests</param>
        /// <param name="cookies"> a string of the path to a text file containing youtube authorization cookies</param>
        /// <returns></returns>
        public IEnumerable<TranscriptItem> GetTranscript(string video_id, IReadOnlyList<string> languages = null, Dictionary<string, string> proxies = null, string cookies = null)
        {
            if (languages == null)
            {
                languages = new List<string>() { "en" };
            }
            //:type proxies: {'http': str, 'https': str} - http://docs.python-requests.org/en/master/user/advanced/#proxies

            return ListTranscripts(video_id, proxies, cookies).FindTranscript(languages).Fetch();
        }

        internal CookieCollection loadCookies(string cookies, string video_id)
        {
            // https://stackoverflow.com/questions/15788341/import-cookies-from-text-file-in-c-sharp-window-forms
            // https://stackoverflow.com/questions/12024657/cookiecontainer-confusion
            // https://stackoverflow.com/questions/8245034/httpwebrequest-redirect-with-cookies

            try
            {
                var cookieCollection = new CookieCollection();
                foreach (var line in NetscapeHttpCookieFileParser(cookies))
                {
                    if (!line.expiration.HasValue || line.expiration.Value < DateTimeOffset.UtcNow)
                    {
                        continue; // Expired
                    }

                    cookieCollection.Add(new Cookie(line.name, line.value, line.path, line.domain));
                }

                if (cookieCollection.Count == 0)
                {
                    throw new CookiesInvalid(video_id);
                }

                return cookieCollection;
            }
            catch (CookiesInvalid)
            {
                throw;
            }
            catch (Exception ex)
            {
                throw new CookiePathInvalid(video_id, ex);
            }
        }
        static bool parseBool(string text)
        {
            switch (text)
            {
                case "TRUE":
                    return true;
                default:
                    return false;
            }
        }

        /// <summary>
        /// Netscape HTTP Cookie File Parser (format)
        /// Parse cookie file a la MozillaCookieJar
        /// </summary>
        /// <param name="pathToFile"></param>
        private static IEnumerable<(string domain, bool? flag, string path, bool? secure, DateTimeOffset? expiration, string name, string value)> NetscapeHttpCookieFileParser(string pathToFile)
        {

            foreach (var line in File.ReadAllLines(pathToFile))
            {
                if (line.StartsWith("#") || string.IsNullOrEmpty(line)) continue;

                /* https://www.hashbangcode.com/article/netscape-http-cooke-file-parser-php
                 * 0 domain - The domain that created and that can read the variable.
                 * 1 flag - A TRUE/FALSE value indicating if all machines within a given domain can access the variable. 
                 *    This value is set automatically by the browser, depending on the value you set for domain.
                 * 2 path - The path within the domain that the variable is valid for.
                 * 3 secure - A TRUE/FALSE value indicating if a secure connection with the domain is needed to access the variable.
                 * 4 expiration - The UNIX time that the variable will expire on.
                 * 5 name - The name of the variable.
                 * 6 value - The value of the variable.
                 */

                var split = line.Split('\t');
                string domain = split[0];
                bool? flag = parseBool(split[1]);
                string path = split[2];
                bool? secure = parseBool(split[3]);
                DateTimeOffset? expiration = null;
                if (long.TryParse(split[4], out var expirationL))
                {
                    expiration = DateTimeOffset.FromUnixTimeSeconds(expirationL);
                }
                string name = split[5];
                string value = split[6];

                yield return (domain, flag, path, secure, expiration, name, value);
            }
        }

        /// <inheritdoc/>
        public void Dispose()
        {
            _httpClient.Dispose();
        }
    }
}
